package capture

import java.util.regex.Matcher
import org.omg.SendingContext.RunTime

class StraceLogParser {
    def syscalls = [
            'accept',   //returns fd - socket, set fd as 'socket'
            'clone',    //need to monitor if fdtable is cloned or shared
            'close',    //closes fd
            'dup',      //make copy of fd, ret lowest possiblie fd
            'dup2',     //make copy of fd, ret newfd from params
            'execve',   //executes process from path
            'fcntl64',  //manipulate fd, (fd,cmd), need to monitor F_DUPFD, F_DUPFD_CLOEXEC
            'open',     // open file, ret fd, is there a need to monitor flags?
            'creat',    // open with O_CREAT|O_WRONLY|O_TRUNC
            'pipe',     // need to monitor fd pair and set them to 'pipe' (or maybe new table?)
            'read',     //read from fd
            'readv',    //like read but to moltiple buffers
            'socket',   //gets fd for socket
            'write',    //write to a file
            'writev',    //---||----
            'unlink',
            'socketpair'
    ]

    /* matcher[0][x]   fields
   * 0  all         15446 open("/usr/lib/locale/en_US.UTF-8/LC_MEASUREMENT", O_RDONLY) = -1 ENOENT (No such file or directory)
   * 1 	pid         15446
   * 2  syscall    	open
   * 3 	params      "/usr/lib/locale/en_US.UTF-8/LC_MEASUREMENT", O_RDONLY
   * 4 	ret         -1
   * 5 	retCode     ENOENT
   * 6 	retVerbose   (No such file or directory)
   * */
    def pattern = ~/^(\d*)\s*(.*?)\((.*)\)\s*=\s*(-?\d*)\s*(\w+)?\s*?(.*$)?/

    int lineno = 0
    OptionManager options
    XmlMessageFactory message

    StraceLogParser() {
        options = OptionManager.getInstance()
        message = XmlMessageFactory.getInstance()
    }

    //running strace passing file name to parser
    void start() {
        def fileName = runStrace()
        println "${new Date().format('HH:mm:ss')} RunStrace returned: $fileName"
        parse(fileName)
    }

    //killing last child of parent, ugly way, (mayby kill all?)
    private void killProgram(String pid) {
        def tmp = pid
        def parent = pid
        while (1) {
            tmp = parent
            parent = "pgrep -P $parent".execute().text
            if (parent == '') {
                "kill $tmp".execute()
                break
            }
        }
    }

    //running strace
    private String runStrace() {
        println "${new Date().format('HH:mm:ss')} RunStrace"
        Date timestamp = new Date()
        def cmd = options.buildCommand()
        options.urlMap.each {it.value = timestamp.format("dd/MM/yyyy HH:mm:ss")} //sets url visit time
        def strace = "strace -o ${timestamp.time} -e trace=${syscalls.join(',')} -f $cmd"
        def proc = strace.execute(null, new File(System.getenv("HOME") + '/capture'))


        Thread.sleep((options.programTimeout ?: 10) * 1000)
        killProgram("${proc.pid}")

        options.visited = 1
        println "${new Date().format('HH:mm:ss')} Executed[${cmd.tokenize()[0]}]: $strace"
        Thread.sleep(500)
        println "${new Date().format('HH:mm:ss')} returning from runStrace"
        return "${timestamp.time}".toString()
    }

    //strace log parser
    private void parse(String logFileName) {
        def client = CaptureClient.getInstance()
        File logFile = new File("/home/jars/capture/$logFileName")
        Matcher match
        logFile.eachLine { line ->
            ++lineno //line counter debugging purpose only
            if (!client.run) throw new RuntimeException("${new Date().format('HH:mm:ss')} Processing interrupted")

            switch (line) {
                case ~/^((\d*)\s*.*)\s\<unfinished \.\.\.\>$/:      //unfinished lines
                    match = Matcher.lastMatcher
                    options.unfinishedLines[match[0][2]] = match[0][1]
                    break
                case ~/^(\d*)\s*\<\.\.\.\s\w*\sresumed\>\s*(.*)/:            //resumed lines
                    match = Matcher.lastMatcher
                    handleLine(options.unfinishedLines[match[0][1]] + match[0][2])

                    break
                case ~/^(\d*)\s*(.*?)\((.*)\)\s*=\s*(-?\d*)\s*(\w+)?\s*?(.*$)?/:    //normal lines
                    handleLine(Matcher.lastMatcher)
                    break
                default:
                    println line
            }
        }

        CaptureClient.getInstance().send(message.visitEvent(VisitEventType.FINISH))
    }

    //sending line to doHandleLine
    private void handleLine(def line) {
        if (line instanceof java.util.regex.Matcher) {
            doHandleLine(line)
        } else {
            println "line: [$line]"
            Matcher matcher = pattern.matcher(line)
            doHandleLine(matcher)
        }

    }

    private void doHandleLine(Matcher matcher) {
        //inits structures needed for monitoring
        if (options.init) {
            options.processMap = [:]
            options.fdMap = [:]
            options.specialFdMap = [:]
            options.cloexecMap = [:]
            options.unfinishedLines = [:]
            options.fdMap[matcher[0][1]] = [:]
            options.specialFdMap[matcher[0][1]] = [:]
            options.cloexecMap[matcher[0][1]] = []
            options.init = false

        }

        def args
        //big switch - take actions depending on syscall name
        //usually its: check return value, remember actions taken(fds opened, closed), check on exclusion list
        switch (matcher[0][2]) {
            case 'accept':
                if (matcher[0][4] == '-1') break
                options.specialFdMap[matcher[0][1]][matcher[0][4]] = 'socket'
                break
            case 'clone':
                if (matcher[0][4] == '-1') break
                args = parseArguments(matcher[0][3])
                if (args[1].indexOf('CLONE_FILES') > 0) {
                    options.fdMap[matcher[0][4]] = options.fdMap[matcher[0][1]]
                    options.specialFdMap[matcher[0][4]] = options.specialFdMap[matcher[0][1]]
                } else {
                    options.fdMap[matcher[0][4]] = new LinkedHashMap(options.fdMap[matcher[0][1]])
                    options.specialFdMap[matcher[0][4]] = new LinkedHashMap(options.specialFdMap[matcher[0][1]])
                }
                options.cloexecMap[matcher[0][4]] = options.cloexecMap[matcher[0][1]]
                options.processMap[matcher[0][4]] = options.processMap[matcher[0][1]]
                break
            case 'close':
                if (matcher[0][4] == '-1') break
                if (!options.fdMap[matcher[0][1]][matcher[0][3]]) {
                    options.specialFdMap[matcher[0][1]].remove(matcher[0][3])
                } else {
                    options.fdMap[matcher[0][1]].remove(matcher[0][3])
                }
                break
            case ['dup', 'dup2']:
                if (matcher[0][4] == '-1') break
                args = parseArguments(matcher[0][3])
                options.fdMap[matcher[0][1]][matcher[0][4]] = options.fdMap[matcher[0][1]][args[0]]
                break
            case 'execve':
                if (matcher[0][4] == '-1') break
                args = parseArguments(matcher[0][3])
                boolean excl = options.isExcluded(ExclusionListType.PROCESS, ['object': args[0], 'subject': options.processMap[matcher[0][1]] ?: args[0]])
                boolean incl = options.isIncluded(ExclusionListType.PROCESS, ['object': args[0], 'subject': options.processMap[matcher[0][1]] ?: args[0]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.PROCESS, matcher[0][1], SystemEventType.CREATE, "${args[0]} ${args[1]}", matcher[0][4]))
                }
                options.cloexecMap[matcher[0][1]].each {
                    options.fdMap[matcher[0][1]].remove(it)
                }
                options.cloexecMap[matcher[0][1]] = []
                options.processMap[matcher[0][1]] = args[0]
                break
            case 'fcntl64':
                if (matcher[0][4] == '-1') break
                args = parseArguments(matcher[0][3])
                if (args[1].indexOf('DUPFD') >= 0) {
                    options.fdMap[matcher[0][1]][matcher[0][4]] = options.fdMap[matcher[0][1]][args[0]]
                }
                if (args[1].indexOf('CLOEXEC') >= 0) {
                    options.cloexecMap[matcher[0][1]].push(matcher[0][4])
                }
                break
            case 'open':
                args = parseArguments(matcher[0][3])
                if (matcher[0][4] != '-1') {
                    options.fdMap[matcher[0][1]][matcher[0][4]] = args[0]
                    if (args[1].indexOf('CLOEXEC') >= 0) {
                        options.cloexecMap[matcher[0][1]].push(matcher[0][4])
                    }
                }
                boolean excl = options.isExcluded(ExclusionListType.FILE, ['action': 'Open', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                boolean incl = options.isIncluded(ExclusionListType.FILE, ['action': 'Open', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.FILE, matcher[0][1], SystemEventType.OPEN, args[0], matcher[0][4]))
                }
                break
            case 'creat':
                args = parseArguments(matcher[0][3])
                if (matcher[0][4] != '-1') {
                    options.fdMap[matcher[0][1]][matcher[0][4]] = args[0]
                    if (args[1].indexOf('CLOEXEC') >= 0) {
                        options.cloexecMap[matcher[0][1]].push(matcher[0][4])
                    }
                }
                boolean excl = options.isExcluded(ExclusionListType.FILE, ['action': 'Create', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                boolean incl = options.isIncluded(ExclusionListType.FILE, ['action': 'Create', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.FILE, matcher[0][1], SystemEventType.CREATE, args[0], matcher[0][4]))
                }
                break
            case 'pipe':
                if (matcher[0][4] == '-1') break
                args = parseArguments(matcher[0][3])
                def fds = args[0] =~ /\[(\d*)[^\d]*(\d*)\]/
                options.specialFdMap[matcher[0][1]][fds[0][1]] = 'pipe'
                options.specialFdMap[matcher[0][1]][fds[0][2]] = 'pipe'
                break
            case ['read', 'readv']:
                args = parseArguments(matcher[0][3])
                if (!options.fdMap[matcher[0][1]][args[0]]) {
                    if (args[0] in ['0', '1', '2'] || args[0] in options.specialFdMap[matcher[0][1]].keySet()) {
                        break
                    } else {
                        //if there are unmatched fds it means that we missed a syscall definition operating on fd
                        println "unmatched[$lineno]: " + matcher[0][0]
                        break
                    }
                }
                boolean excl = options.isExcluded(ExclusionListType.FILE, ['action': 'Read', 'path': options.fdMap[matcher[0][1]][args[0]], 'subject': options.processMap[matcher[0][1]]])
                boolean incl = options.isIncluded(ExclusionListType.FILE, ['action': 'Read', 'path': options.fdMap[matcher[0][1]][args[0]], 'subject': options.processMap[matcher[0][1]]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.FILE, matcher[0][1], SystemEventType.READ, options.fdMap[matcher[0][1]][args[0]], matcher[0][4]))
                }
                break
            case 'socket':
                if (matcher[0][4] == '-1') break
                options.specialFdMap[matcher[0][1]][matcher[0][4]] = 'socket'
                break
            case ['write', 'writev']:
                args = parseArguments(matcher[0][3])
                if (!options.fdMap[matcher[0][1]][args[0]]) {
                    if (args[0] in ['0', '1', '2'] || args[0] in options.specialFdMap[matcher[0][1]].keySet()) {
                        break
                    } else {
                        println "unmatched[$lineno]: " + matcher[0][0]
                        break
                    }
                }
                boolean excl = options.isExcluded(ExclusionListType.FILE, ['action': 'Write', 'path': options.fdMap[matcher[0][1]][args[0]], 'subject': options.processMap[matcher[0][1]]])
                boolean incl = options.isIncluded(ExclusionListType.FILE, ['action': 'Write', 'path': options.fdMap[matcher[0][1]][args[0]], 'subject': options.processMap[matcher[0][1]]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.FILE, matcher[0][1], SystemEventType.WRITE, options.fdMap[matcher[0][1]][args[0]], matcher[0][4]))
                }
                break
            case 'unlink':
                args = parseArguments(matcher[0][3])
                boolean excl = options.isExcluded(ExclusionListType.FILE, ['action': 'Delete', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                boolean incl = options.isIncluded(ExclusionListType.FILE, ['action': 'Delete', 'path': args[0], 'subject': options.processMap[matcher[0][1]]])
                if (excl || !incl) {
                    OptionManager.getInstance().malicious = 1
                    CaptureClient.getInstance().send(message.systemEvent(SystemEventType.FILE, matcher[0][1], SystemEventType.DELETE, args[0], matcher[0][4]))
                }
                break
            case 'socketpair':
                args = parseArguments(matcher[0][3])
                def sockets = args[3][1..-2].split(', ')
                options.specialFdMap[matcher[0][1]][sockets[0]] = 'socket'
                options.specialFdMap[matcher[0][1]][sockets[1]] = 'socket'
                break
            default:
                println "-------------------- Not matched: " + matcher[0][2]
        }
    }

    //converts syscall arguments to list
    private ArrayList<String> parseArguments(String optionString) {
        def ret = []
        def tmpString = ''
        int brackets = 0
        optionString.each {
            switch (it) {
                case ['[', '{']:
                    ++brackets
                    tmpString += it
                    break
                case [']', '}']:
                    --brackets
                    tmpString += it
                    break
                case ',':
                    if (!brackets) {
                        ret.push(tmpString)
                        tmpString = ''
                    } else {
                        tmpString += it
                    }
                    break
                case '"':
                    break
                case ' ':
                    if (brackets) {
                        tmpString += it
                    }
                    break
                default:
                    tmpString += it
                    break
            }

        }

        ret.push(tmpString)
        return ret

    }
}
